#include "Engine.h"

Engine::Engine(Artist* artistPointer, StateMachine* statePointer)
{
    stateMachine = statePointer;
    artist = artistPointer;
    gravity = 40;
}

bool Engine::onGround(Unit* un)
{
    if(gravity_dir == DOWN && map.solid_collision(DOWN, un -> get_move(DOWN, 1)))
      return true;
    if(gravity_dir == UP && map.solid_collision(UP, un -> get_move(UP, 1)))
      return true;
    if(gravity_dir == LEFT && map.solid_collision(LEFT, un -> get_move(LEFT, 1)))
      return true;
    if(gravity_dir == RIGHT && map.solid_collision(RIGHT, un -> get_move(RIGHT, 1)))
      return true;

    return false;
}

void Engine::update()
{
    // current acceleration, including player movement
    int yAcc = player-> get_yAcc();
    int xAcc = player-> get_xAcc();
    // terminal velocity
    int xlim = 0;
    int ylim = 0;
    // friction
    int xFric = 0;
    int yFric = 0;

    map.unstick_me(player->get_bounds());

    switch(gravity_dir)
    {
//                  -apply gravity-     -set max vel.-       -set friction-
        case DOWN:  yAcc += gravity; ylim = 9; xlim = 6; xFric = map.get_fric(); break;
        case UP:    yAcc -= gravity; ylim = 9; xlim = 6; xFric = map.get_fric(); break;
        case RIGHT: xAcc += gravity; ylim = 6; xlim = 9; yFric = map.get_fric(); break;
        case LEFT:  xAcc -= gravity; ylim = 6; xlim = 9; yFric = map.get_fric();
    }

  xlim = 800;
  ylim = 800;
//   -apply accelerataion to the player's velocity-
    player -> incYVelocity(yAcc);
    player -> incXVelocity(xAcc);

    int xvelocity = player->getXVelocity();
    int yvelocity = player->getYVelocity();

//   -limit to terminal velocity-
    if(yvelocity > ylim)
        yvelocity = ylim;
    else if(yvelocity < -1*ylim)
        yvelocity = -1*ylim;
    if(xvelocity > xlim)
        xvelocity = xlim;
    else if(xvelocity < -1*xlim)
        xvelocity = -1*xlim;

//  -apply friction if sliding / horizontal air resistance if falling-
    if(xAcc || !onGround(player))
        xFric /= 5;
    if(yAcc || !onGround(player))
        yFric /= 5;

    if(xvelocity>0)
    {
        xvelocity -= xFric;
        if(xvelocity<0)
            xvelocity =0;
    }
    else if(xvelocity<0)
    {
        xvelocity += xFric;
        if(xvelocity>0)
            xvelocity =0;
    }

    if(yvelocity>0)
    {
        yvelocity -= yFric;
        if(yvelocity<0)
            yvelocity =0;
    }
    else if(yvelocity<0)
    {
        yvelocity += yFric;
        if(yvelocity>0)
            yvelocity =0;
    }

//  -apply changes to player's velocity-
    player->setXVelocity(xvelocity);
    player->setYVelocity(yvelocity);


//  -y axis movement-
    Direction y_dir = DOWN;

    if(yvelocity<0)
    {
        y_dir = UP;
    }

    for(int i = 0; i < abs(yvelocity)/100; i++)
    {
//      -determine if player can move one pixel-
        if(!map.solid_collision(y_dir, player -> get_move(y_dir, 1)))
        {
        //  -move the player one pixel-
            player -> move(y_dir, 1);
        }
        //collision, but the player is moving orthogonal to gravity
        else if(gravity_dir == RIGHT || gravity_dir == LEFT)
        {
            // try to move uphill
            if(!tryStep(y_dir, i) && !tryStep(y_dir, i, true))
            {
                //up against a wall
                //break from loop and kill y velocity
                i= abs(yvelocity);
                player -> setYVelocity(0);
            }
        }
        //colliding against ground or ceiling
        else
        {
            // kill y velocity and break from loop
            player -> setYVelocity(0);
            i = abs(yvelocity);
        }
    }

//  -x axis movement-
    Direction x_dir = RIGHT;

    if(xvelocity<0)
    {
        x_dir = LEFT;
    }

    for(int i = 0; i < abs(xvelocity/100); i++)
    {
//      -determine if player can move one pixel-
        if(!map.solid_collision(x_dir, player -> get_move(x_dir, 1)))
        {
        //  -move the player one pixel-
            player -> move(x_dir, 1);
        }
        //collision, but the player is moving orthogonal to gravity
        else if(gravity_dir == UP || gravity_dir == DOWN)
        {
            // try to move uphill
            if(!tryStep(x_dir, i) && !tryStep(x_dir, i, true))
            {
                //up against a wall
                //break from loop and kill x velocity
                i = abs(xvelocity);
                player -> setXVelocity(0);
            }
        }
        //colliding against ground or ceiling
        else
        {
            // kill x velocity and break from loop
            player -> setXVelocity(0);
            i = abs(xvelocity);
        }
    }
    player -> setXAcc(0);
    player -> setYAcc(0);
}

/// stairstep algorithm for moving uphill
bool Engine::tryStep(Direction dir, int& i, bool reverse)
{
    // maximum step height
    int step = 3;
    // distance to try at a time (increment)
    // also counts as horizontal movement penalty if step succeeds
    int k = 1;

    bool stepped;
    stepped = false;

    do
    {
        SDL_Rect box = player -> get_move(dir, 1);

        if((!reverse && gravity_dir==RIGHT) || (reverse && gravity_dir==LEFT))
            box.x -= k;
        else if((!reverse && gravity_dir==LEFT) || (reverse && gravity_dir==RIGHT))
            box.x += k;
        else if((!reverse && gravity_dir==UP) || (reverse && gravity_dir==DOWN))
            box.y += k;
        else if((!reverse && gravity_dir==DOWN) || (reverse && gravity_dir==UP))
            box.y -= k;

        // the player is able to make the step
        if(!map.solid_collision(dir, box))
        {
        //  the player steps up
            player -> teleport(box);
            stepped = true;
        }
        k++;
    } while(k<=step && !stepped);

    if(!stepped)
        return false;

    //slow player on steeper surfaces
    //subtracts step height from player's total movement this frame
    i+=k;

    return true;
}

/// test to see if stairstep is possible
bool Engine::canStep(Direction dir)
{
    // maximum step height
    int step = 3;
    // distance to try at a time (increment)
    // also counts as horizontal movement penalty if step succeeds
    int k = 1;

    do
    {
        SDL_Rect box = player -> get_move(dir, 1);

        if(gravity_dir==RIGHT)
            box.x -= k;
        else if(gravity_dir==LEFT)
            box.x += k;
        else if(gravity_dir==UP)
            box.y += k;
        else if(gravity_dir==DOWN)
            box.y -= k;

        // the player is able to make the step
        if(!map.solid_collision(dir, box))
        {
            return true;
        }
        k++;
    } while(k<=step);

    return false;
}

void Engine::display()
{
    SDL_Surface* screen = artist -> getScreen();

    //artist -> clear();

    camera.center_on(*player->get_bounds());
    validateCamera();

    map.drawMap(screen, camera.get_bounds());

    player -> display(*camera.get_bounds(), screen);

    map.drawObjects(camera.get_bounds(), screen, "foreground");
}

void Engine::newGame()
{
    map.loadMap("upvote.map");

    std::string filepath = "sprites/tileset/" + map.get_theme() + ".png";

    map.set_TileSheet(artist, filepath);

    map.loadObjectImages(*artist);

    gravity_dir = DOWN;

    player = new Unit(*artist, "player");

    std::pair<int,int> coords = map.get_start();

    player->teleport(coords.first, coords.second);
}

void Engine::loadGame()
{
    // do load game stuff
}

void Engine::run()
{
    stateMachine -> setState(IN_GAME);

    while(stateMachine -> getState() == IN_GAME)
    {
        check_events();

        update();

        display();

        artist -> flip(gravity_dir);

        artist -> framerate_limit();
    }
}

void Engine::check_events(){
    Uint8 *keystate = SDL_GetKeyState(NULL);
    while( SDL_PollEvent( &event ) )
    {
        if( event.type == SDL_QUIT)
            stateMachine->setState(QUIT);
        else if( event.type == SDL_KEYDOWN )
        {
            switch(event.key.keysym.sym){
                case SDLK_q:
                case SDLK_ESCAPE: stateMachine -> setState(MAIN_MENU); break;
                case SDLK_RETURN:
                                    std::cout << "  STATUS REPORT   \n\n";
                                    std::cout << "XVel: " << player->getXVelocity() << "\n";
                                    std::cout << "yVel: " << player->getYVelocity() << "\n";
                                    std::cout << "XAcc: " << player->get_xAcc() << "\n";
                                    std::cout << "YAcc: " << player->get_yAcc() << "\n\n";
                                    break;
                case SDLK_s:
                case SDLK_w:
                    player->rotateSprite(*artist, UP);
                    switch(gravity_dir)
                    {
                        case UP: gravity_dir = DOWN; break;
                        case RIGHT: gravity_dir = LEFT; break;
                        case DOWN: gravity_dir = UP; break;
                        case LEFT: gravity_dir = RIGHT; break;
                    }
                    break;

                case SDLK_a:
                    player->rotateSprite(*artist, LEFT);
                    switch(gravity_dir)
                    {
                        case UP: gravity_dir = RIGHT; break;
                        case RIGHT: gravity_dir = DOWN; break;
                        case DOWN: gravity_dir = LEFT; break;
                        case LEFT: gravity_dir = UP; break;
                    }
                    camera.swap();
                    break;

                case SDLK_d:
                    player->rotateSprite(*artist, RIGHT);
                    switch(gravity_dir)
                    {
                        case UP: gravity_dir = LEFT; break;
                        case RIGHT: gravity_dir = UP; break;
                        case DOWN: gravity_dir = RIGHT; break;
                        case LEFT: gravity_dir = DOWN; break;
                    }
                    camera.swap();
                    break;
                case SDLK_z: playerJump(); break;
                default: ;
            }
        }
    }

    if(keystate[SDLK_RIGHT])
        player->walk(gravity_dir, RIGHT);
    else if(keystate[SDLK_LEFT])
        player->walk(gravity_dir, LEFT);
    else
        player->stand();
}

void Engine::playerJump()
{
    Uint8 *keystate = SDL_GetKeyState(NULL);

    Direction first;
    Direction first_op;
    Direction second;
    Direction second_op;
    bool primary = false;
    bool diagonal = false;

    bool ar_L = false;
    bool ar_R = false;
    bool ar_U = false;
    bool ar_D = false;

    if((gravity_dir == DOWN && keystate[SDLK_LEFT])  || (gravity_dir == UP && keystate[SDLK_RIGHT]))
        ar_L = true;
    if((gravity_dir == DOWN && keystate[SDLK_RIGHT]) || (gravity_dir == UP && keystate[SDLK_LEFT]))
        ar_R = true;
    if((gravity_dir == LEFT && keystate[SDLK_LEFT])  || (gravity_dir == RIGHT && keystate[SDLK_RIGHT]))
        ar_U = true;
    if((gravity_dir == LEFT && keystate[SDLK_RIGHT]) || (gravity_dir == RIGHT && keystate[SDLK_LEFT]))
        ar_D = true;

    if(!keystate[SDLK_DOWN])
    {
        first = gravity_dir;
        primary = true;

        switch(gravity_dir)
        {
            case UP: first_op = DOWN; break;
            case DOWN: first_op = UP; break;
            case RIGHT: first_op = LEFT; break;
            case LEFT: first_op = RIGHT;
        }
    }

    if((!keystate[SDLK_LEFT] && keystate[SDLK_RIGHT]) || (keystate[SDLK_LEFT] && !keystate[SDLK_RIGHT]))
    {
        diagonal = true;
        if(ar_L)
        {
            second = RIGHT;
            second_op = LEFT;
        }
        else if(ar_R)
        {
            second = LEFT;
            second_op = RIGHT;
        }
        else if(ar_D)
        {
            second = UP;
            second_op = DOWN;
        }
        else if(ar_U)
        {
            second = DOWN;
            second_op = UP;
        }

        if(player->get_walljump() && !canStep(second_op))
        {
            Direction dummy = second;
            second = second_op;
            second_op = dummy;
        }

    }
    if(primary && map.solid_collision(first, player -> get_move(first, 1)))
        player->jump(first_op);
    if(diagonal && map.solid_collision(second, player -> get_move(second, 1)))
        player->jump(second_op);
}

void Engine::validateCamera()
{
    if(camera.get_x() < 0)
        camera.set_x(0);
    else if(camera.get_x() + camera.get_w() > map.get_Widthpx())
        camera.set_x(map.get_Widthpx() - camera.get_w());
    if(camera.get_y() < 0)
        camera.set_y(0);
    else if(camera.get_y() + camera.get_h() > map.get_Heightpx())
        camera.set_y(map.get_Heightpx() - camera.get_h());
}
